iT邦幫忙

2024 iThome 鐵人賽

DAY 2
0
Python

Python 錦囊密技系列 第 2

【Python錦囊㊙️技2】Python 很簡單,但要寫的好很不簡單

  • 分享至 

  • xImage
  •  

前言

撰寫Python程式很簡單,因為Python的發明人Guido van Rossum希望撰寫Python程式可以像英文寫作一樣自然,不需要作變數型別宣告、語法簡單有彈性,Guido在Python直譯器暗藏【Python之禪】(The Zen of Python),闡述Python的理念,只要輸入【import this】就可以看到10項訴求,中文翻譯可參照維基百科:
https://ithelp.ithome.com.tw/upload/images/20240916/20001976wc6BAQKArw.png
圖一. Python之禪,由Tim Peters提出(PEP20)

但是,要開發一個好的程式,絕對不能很浪漫的coding,否則會造成很嚴重的後遺症,程式不只要能被正確執行,還要考慮上一篇所提到的軟體品質7大指標,因此,就以開發小型計算機程式為例,說明多種設計模式的運用,包括OOP、Functional programming、Lambda function及Closure等。

Tkinter入門

筆者使用Tkinter開發UI,它是Python內建模組,不需任何安裝,最簡單的程式如下:

  1. 建立一個視窗:將下列程式存成calc0.py。
from tkinter import *

# 建立 tkinter 物件,自動產生新視窗
app = Tk() 

# 監聽訊息 
app.mainloop() 
  1. 執行:python calc0.py

  2. 執行結果:
    https://ithelp.ithome.com.tw/upload/images/20240914/20001976Vl7pLdgO3i.png

  3. 再增加一個按鈕,使用表格(Grid)切版:

from tkinter import *

# create a GUI window 
app = Tk() 

# 建立按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue', 
                height=1, width=7) 
button1.grid(row=0, column=0) 

# 監聽訊息 
app.mainloop() 

執行結果:
https://ithelp.ithome.com.tw/upload/images/20240914/20001976UoyZsA1L0i.png

  1. 再增加一個輸入框(Entry),並指定一個變數(equation),與輸入框對應,可透過變數存取輸入框的內容:
from tkinter import *

# create a GUI window 
app = Tk() 

# 表達式
equation = StringVar() 
expression_field = Entry(app, textvariable=equation) 
# 欄寬
expression_field.grid(columnspan=4, ipadx=70) 

# 建立按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue', 
                height=1, width=7) 
button1.grid(row=2, column=0) 

# 監聽訊息 
app.mainloop() 

執行結果:
https://ithelp.ithome.com.tw/upload/images/20240914/20001976KmYo4IuO85.png

  1. 最後為按鈕增加事件處理函數(press),並在按鈕定義加上【command=lambda: press(1)】,表示點擊按鈕會呼叫press(1),對輸入框插入【1】。
from tkinter import *

# 按鈕事件處理
def press(num): 
    global expression 
    expression_field.insert(END, str(num))

# create a GUI window 
app = Tk() 

# 表達式
equation = StringVar() 
expression_field = Entry(app, textvariable=equation) 
# 欄寬
expression_field.grid(columnspan=4, ipadx=70) 

# 建立按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue', 
                command=lambda: press(1), height=1, width=7) 
button1.grid(row=2, column=0) 

# 監聽訊息 
app.mainloop() 

開發小型計算機程式

筆者初學Python時,透過開發小型計算機程式學習Python,檔名為calc1.py,程式很囉唆,不必細看。

  1. 先建立使用者介面:主要就是設定0~9、+-*/、及其他按鈕。
from tkinter import *

# create a GUI window 
app = Tk() 

# set the configuration of GUI window 
app.geometry("280x180") 

# 表達式
equation = StringVar() 
expression_field = Entry(app, textvariable=equation) 
# 欄寬設定
expression_field.grid(columnspan=4, ipadx=70) 

# 建立0~9按鈕
button1 = Button(app, text=' 1 ', fg='black', bg='lightblue', 
                command=lambda: press(1), height=1, width=7) 
button1.grid(row=2, column=0) 

button2 = Button(app, text=' 2 ', fg='black', bg='lightblue', 
                command=lambda: press(2), height=1, width=7) 
button2.grid(row=2, column=1) 

button3 = Button(app, text=' 3 ', fg='black', bg='lightblue', 
                command=lambda: press(3), height=1, width=7) 
button3.grid(row=2, column=2) 

button4 = Button(app, text=' 4 ', fg='black', bg='lightblue', 
                command=lambda: press(4), height=1, width=7) 
button4.grid(row=3, column=0) 

button5 = Button(app, text=' 5 ', fg='black', bg='lightblue', 
                command=lambda: press(5), height=1, width=7) 
button5.grid(row=3, column=1) 

button6 = Button(app, text=' 6 ', fg='black', bg='lightblue', 
                command=lambda: press(6), height=1, width=7) 
button6.grid(row=3, column=2) 

button7 = Button(app, text=' 7 ', fg='black', bg='lightblue', 
                command=lambda: press(7), height=1, width=7) 
button7.grid(row=4, column=0) 

button8 = Button(app, text=' 8 ', fg='black', bg='lightblue', 
                command=lambda: press(8), height=1, width=7) 
button8.grid(row=4, column=1) 

button9 = Button(app, text=' 9 ', fg='black', bg='lightblue', 
                command=lambda: press(9), height=1, width=7) 
button9.grid(row=4, column=2) 

button0 = Button(app, text=' 0 ', fg='black', bg='lightblue', 
                command=lambda: press(0), height=1, width=7) 
button0.grid(row=5, column=0) 

plus = Button(app, text=' + ', fg='black', bg='lightblue', 
            command=lambda: press("+"), height=1, width=7) 
plus.grid(row=2, column=3) 

# 建立【+-*/】按鈕
minus = Button(app, text=' - ', fg='black', bg='lightblue', 
            command=lambda: press("-"), height=1, width=7) 
minus.grid(row=3, column=3) 

multiply = Button(app, text=' * ', fg='black', bg='lightblue', 
                command=lambda: press("*"), height=1, width=7) 
multiply.grid(row=4, column=3) 

divide = Button(app, text=' / ', fg='black', bg='lightblue', 
                command=lambda: press("/"), height=1, width=7) 
divide.grid(row=5, column=3) 

equal = Button(app, text=' = ', fg='black', bg='lightblue', 
            command=calc, height=1, width=7) 
equal.grid(row=5, column=2) 

# 建立其他按鈕
clear1 = Button(app, text='Clear', fg='black', bg='lightblue', 
            command=clear, height=1, width=7) 
clear1.grid(row=5, column='1') 

Decimal= Button(app, text='.', fg='black', bg='lightblue', 
                command=lambda: press('.'), height=1, width=7) 
Decimal.grid(row=6, column=0) 


# 監聽訊息  
app.mainloop() 
  1. 再撰寫按鈕的事件處理程式碼:包括計算、清除表達式,計算很簡單,使用eval函數,可以很輕易的評估輸入框內容。
# 按鈕事件處理
def press(num): 
    global expression 
    expression_field.insert(END, str(num))

# 清除輸入列    
def clear(): 
    global expression 
    expression = "" 
    equation.set("") 

# 計算
def calc(): 
    try: 
        global expression 
        total = str(eval(equation.get())) 
        equation.set(total) 
        expression = str(total) 
    except: 
        equation.set(" error ") 
        expression = "" 
  1. 執行:python calc1.py。

  2. 結果如下:
    https://ithelp.ithome.com.tw/upload/images/20240914/20001976QbBJfEG7Bi.png

改良

上述程式看起來很OK,但是程式碼又臭又長,很難維護,很像小學生寫作文,平鋪直敘😅。
將程式進行重構(Refactoring),包括:

  1. 將0~9、+-*/=等按鈕以迴圈處理。
  2. UI以獨立的類別(Class)實作。
  3. 以Closure設計理念定義按鈕的事件處理函數。

修改後的檔名為calc_fp.py,重點如下:

  1. 繼承(Inheritance):類別 App 繼承 tkinter.Tk 類別,在類別內完成UI創建及相關事件處理。
class App(tk.Tk):
  1. 以迴圈創建0~9、+-*/=等按鈕,並以閉包(Closure)定義點擊按鈕時的處理函數,整個程式就變得很簡潔、有條理。
import tkinter as tk
from tkinter import ttk
from functools import partial

COLUMN_COUNT = 5   # 行數
FONT=('Arial', 16) # 字體
class App(tk.Tk):
    def __init__(self):
        # 呼叫 parent 的 __init__ 函數
        super().__init__() 
        
        # set window size
        self.geometry("")
        
        # set window title
        self.title('計算機')
              
        # create label
        self.label1 = tk.Label(self, text='輸入:', font=FONT)
        self.label1.grid(row=0, column=0)
        
        # create entry field
        self.equation = tk.StringVar() 
        self.expression_field = tk.Entry(self, textvariable=self.equation
        , font=FONT, borderwidth = 5) 
        self.expression_field.grid(row=0, column=1, columnspan=COLUMN_COUNT-1, ipadx=32)
        
        # create 0~9 buttons
        button_list = []
        for i in range(10):
            button_list.append(tk.Button(self, text=f' {i} ', 
                command=partial(self.button_click,i), 
                fg='black', bg='lightblue', height=1, width=8, font=FONT))
            grid_row, grid_column = divmod(i, COLUMN_COUNT)
            button_list[-1].grid(row=grid_row+1, column=grid_column) 
        
        # create '+', '-', '*', '/' buttons
        for j, text in enumerate(list('+-*/()=c')):
            button_list.append(tk.Button(self, text=f' {text} ', 
                command=partial(self.button_click,text), 
                fg='black', bg='lightgreen', height=1, width=8, font=FONT))
            grid_row2, grid_column = divmod(j, COLUMN_COUNT)
            button_list[-1].grid(row=grid_row+grid_row2+2, column=grid_column) 

        # error message
        self.message = tk.StringVar() 
        self.label_message = tk.Label(self, text='', textvariable = self.message)
        self.label_message.grid(row=grid_row+grid_row2+3, column=0, columnspan=COLUMN_COUNT)

    # button click event handler
    def button_click(self, num):
        if num == '=':
            # 計算
            try:
                # 顯示答案
                self.equation.set(str(eval(self.equation.get())))
            except Exception as e: # 錯誤處理
                self.message.set(repr(e)[:80])
        elif num == 'c':
            self.equation.set('') 
            self.message.set('') 
        else:
            self.expression_field.insert(tk.END, str(num))

if __name__ == "__main__":
    app = App()
    app.mainloop()
  1. 其中以迴圈創建0~9按鈕的程式碼如下,第三行的partial就是Closure的處理方式,可以將函數及參數i的狀態保存下來,傳給後續的事件處理函數。
        for i in range(10):
            button_list.append(tk.Button(self, text=f' {i} ', 
                command=partial(self.button_click, i), 
                fg='black', bg='lightblue', height=1, width=8, font=FONT))
            grid_row, grid_column = divmod(i, COLUMN_COUNT)

讀者可以依照calc1.py的寫法,將【partial(self.button_click,i)】改為【lambda: self.button_click(i)】試試看,筆者將之另存為calc_error.py,執行結果會發現點擊0~9按鈕都會出現9,這是因為定義command時,只保存函數及參數名稱,並未將i當時的狀態(0~9)保留,會等到點擊0~9按鈕時才去取值,那時迴圈已執行結束,i等於9,因此每個按鈕處理函數都取得9,這就是Closure的重要性,而Python內建的函數partial,可以記錄參數當時的值。

結語

由上面的程式碼可以觀察到,使用Python開發程式真的很簡單,但是要寫的好,真的需要更深入的了解Python才行,例如上述程式就用了繼承(Inheritance)、匿名函數(Lambda expression)、閉包(Closure)等設計理念,Python還有更多的寶藏等待我們去發掘,接下來就請讀者與筆者一起探討Python之美。

以上的程式只是展示各種設計理念的用法,後續我們會詳細介紹各種設計理念。

本系列的程式碼會統一放在GitHub,本篇的程式放在src/2資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。


上一篇
【Python錦囊㊙️技1】開發中大型應用系統的必備技能
下一篇
【Python錦囊㊙️技3】One liners
系列文
Python 錦囊密技30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言